/**
 *  JFTSTool - Java Full Text Search tool.
 *  Copyright (C) <2009>  <grupo JFTS>
 *  This program is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jftstool.searcher;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RangeQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopFieldDocCollector;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.jftstool.analyzers.AnalyzerManager;
import org.jftstool.analyzers.JFTSAnalyzer;
import org.jftstool.analyzers.exceptions.AnalyzerManagerException;
import org.jftstool.config.JFTSConfiguration;
import org.jftstool.config.OrderConfig;
import org.jftstool.config.OrdersConfig;
import org.jftstool.config.exceptions.JFTSConfigException;
import org.jftstool.enums.OrderTypes;
import org.jftstool.exceptions.JFTSFrameworkException;
import org.jftstool.result.IJFTSResultItem;
import org.jftstool.result.JFTSResult;
import org.jftstool.resultOrder.ResultOrder;
import org.jftstool.searcher.addons.MoreLikeThis;
import org.jftstool.searcher.exceptions.JFTSSearcherException;
import org.jftstool.semantic.ISemanticModel;
import org.jftstool.semantic.JFTSHierarchy;
import org.jftstool.semantic.JFTSHierarchyNode;
import org.jftstool.semantic.exceptions.JFTSSemanticException;

import com.hp.hpl.jena.ontology.OntClass;
import com.hp.hpl.jena.util.iterator.Filter;

/**
 * Class that represents the process of finding information.
 * @author grupo JFTS
 *
 */
public class JFTSSearcher implements IJFTSSearcher {
	private JFTSConfiguration config = null;
	static Logger logger = Logger.getLogger(JFTSSearcher.class);   	
	private Searcher is = null;
	private IndexReader ir = null;
	private ISemanticModel sm = null;
	private int maxDocumentReturn = 0;
	
	public JFTSSearcher(Searcher is, IndexReader ir, ISemanticModel sm) throws JFTSSearcherException {
		try {
			config = JFTSConfiguration.getInstance();
			DOMConfigurator.configure("resources/log4j/log4jConfig.xml");
			this.is = is;
			this.ir = ir;
			this.sm = sm;
			maxDocumentReturn = config.getSearcherConfig().getMaxDocumentReturn();
		}
		catch (JFTSConfigException e) {
			throw new JFTSSearcherException(e.getMessage());
		}
	}
	
	/**
	 * Set the IndexSearcher to use during search process.
	 * @param is
	 */
	public void setIndexSearcher(Searcher is){
		this.is = is;
	}
	
	/**
	 * Set the IndexReader to use during search process.
	 * @param ir
	 */	
	public void setIndexReader(IndexReader ir){
		this.ir = ir;
	}
	
	@Override
	public JFTSResult prefixQuerySearch(String field, String query, ResultOrder order, int page, int resultsPerPage)
			throws JFTSSearcherException, JFTSFrameworkException {
		
		Term t = new Term(field, query);
		Query q = new PrefixQuery(t);

		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;		
	}

	
	@Override
	public JFTSResult simpleQuerySearch(String field, String query, ResultOrder order, int page, int resultsPerPage)
			throws JFTSSearcherException, JFTSFrameworkException {
		
		Term t = new Term(field, query);
		Query q = new TermQuery(t);

		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;
	}


	@Override
	public Document getDocument(int id) throws JFTSSearcherException,
			JFTSFrameworkException {
		
		Document doc = null;
		try {
			doc =  is.doc(id);
		}
		catch (CorruptIndexException e) {
			e.printStackTrace();
			throw new JFTSSearcherException("Error, index is corrupt");
			
		}
		catch (IOException e) {
			throw new JFTSSearcherException("Error, can't read index, I/O exception");			
		}
		return doc;
	}


	@Override
	public JFTSResult rangeQuerySearch(String field, String begin, String end, boolean inclusive, ResultOrder order, int page, int resultsPerPage)
			throws JFTSSearcherException, JFTSFrameworkException {

		Term a = new Term(field, begin);
		Term b = new Term(field, end);		
		Query q = new RangeQuery(a, b, inclusive);

		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;		
	}


	@Override
	public JFTSResult phraseQuerySearch(String field, String[] phrase, int slop, ResultOrder order, int page, int resultsPerPage)
			throws JFTSSearcherException, JFTSFrameworkException {
		
		PhraseQuery q = new PhraseQuery();
		q.setSlop(slop);
		
		if (phrase != null){
			for (int i=0; i < phrase.length; i++) {
				q.add(new Term(field, phrase[i]));
			}
		}
		
		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;		
	}


	@Override
	public JFTSResult phraseQuerySearch(String field, String phrase,
			String regEx, int slop, ResultOrder order, int page, int resultsPerPage) throws JFTSSearcherException,
			JFTSFrameworkException {


		PhraseQuery q = new PhraseQuery();
		q.setSlop(slop);

		if (regEx == null || regEx.isEmpty()){
			regEx = " ";
		}
		
		String[] words = phrase.split(regEx);
		
		if (words != null){
			for (int i=0; i < words.length; i++) {
				q.add(new Term(field, words[i]));
			}
		}
		
		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;		
	}


	@Override
	public JFTSResult wildCardQuerySearch(String field, String query, ResultOrder order, int page, int resultsPerPage)
			throws JFTSSearcherException, JFTSFrameworkException {

		Term t = new Term(field, query);
		Query q = new WildcardQuery(t);
		
		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;		
		
	}


	@Override
	public JFTSResult similarityQuerySearch(String field, String query, ResultOrder order, int page, int resultsPerPage)
			throws JFTSSearcherException, JFTSFrameworkException {
		
		Term t = new Term(field, query);
		Query q = new FuzzyQuery(t);
		
		JFTSResult result = _executeQuery(q, order, page, resultsPerPage);		
		
		return result;		
		
	}


	@Override
	public JFTSResult humanQuerySearch(String query, ResultOrder order, int page, int resultsPerPage, String language) throws JFTSSearcherException,
			JFTSFrameworkException {
		JFTSResult result = null;
		JFTSAnalyzer analyzer = _getAnalyzer(language);
		if (analyzer == null){
			throw new JFTSSearcherException("Error, can't find analyzer for specified language");
		}
		else{
			QueryParser qp = new QueryParser("content", analyzer);
			try {
				Query q = qp.parse(query);
				System.out.println("CONSULTA A EJECUTAR: " + q.toString());
				result = _executeQuery(q, order, page, resultsPerPage);		
			}
			catch (ParseException e) {
				throw new JFTSSearcherException(e.getMessage());
			}
		}
		return result;
	}

	
	@Override
	public TopDocs moreLikeThisQuery(int docId, String[] fields,
			ResultOrder order) throws JFTSSearcherException, JFTSFrameworkException {
		
		String indexPath = null;
		IndexReader ir = null;
		TopDocs res = null;
		try {
			Query q = null;
			Sort o = null;
			
			indexPath = config.getIndexerConfig().getIndexPath();
			Directory dir = FSDirectory.getDirectory(indexPath);
			ir = IndexReader.open(dir);
			
			MoreLikeThis mlt = new MoreLikeThis(ir);
			mlt.setFieldNames(fields);
			mlt.setMinWordLen(2);
			mlt.setBoost(true);
			
			q = mlt.like(docId);
			o = _processResultOrder(order);
			res = is.search(q, null, maxDocumentReturn, o);				

		}
		catch (IOException e) {
			throw new JFTSSearcherException("Error to open index. Index dir: " + indexPath);			
		}
		catch (JFTSConfigException e) {
			throw new JFTSSearcherException(e.getMessage());
		}
		finally{
			if (ir != null){
				try {
					ir.close();
				}
				catch (Exception e) { /* do nothing */}
			}
		}		
		return res;
	}	
	
	

	@Override
	public ResultOrder getValidOrders()
			throws JFTSSearcherException, JFTSFrameworkException {
		
		ResultOrder result = null;
		
		try {
			OrdersConfig ordersConfig = config.getSearcherConfig().getOrders();
			List<OrderConfig> orders = ordersConfig.getOrders();
			if (orders != null && orders.size() > 0){
				List<Map<String, String>> aux = new ArrayList<Map<String, String>>();
				for (OrderConfig o : orders){
					aux.add(o.getArgs());
				}
				result = new ResultOrder(aux);
			}
		}
		catch (JFTSConfigException e) {
			throw new JFTSSearcherException(e.getMessage());
		}
		return result;
	}
	
	
	
	@Override
	public JFTSHierarchy getOntoCategorization() throws JFTSSearcherException,
			JFTSFrameworkException {
		
		// create an iterator over the root classes that are not anonymous class expressions
        Iterator<OntClass> i = sm.getBaseModel().listHierarchyRootClasses()
        	.filterDrop( new Filter<OntClass>() {
        		@Override
                public boolean accept( OntClass r ) {
        			return r.isAnon();
        }} );
        JFTSHierarchyNode node = null;
        List<JFTSHierarchyNode> aux = new ArrayList<JFTSHierarchyNode>();
        while (i.hasNext()) {
        	OntClass cls = i.next();
        	List<OntClass> occurs = new ArrayList<OntClass>();
        	node = _getSubClasses(cls, occurs);
        	if (node != null){
        		aux.add(node);
        	}
        }
        
        JFTSHierarchyNode root = new JFTSHierarchyNode("JFTSROOT", 0, aux);
        JFTSHierarchy struct = new JFTSHierarchy(root);
		return struct;
	}
	
	

    private JFTSHierarchyNode _getSubClasses( OntClass cls, List<OntClass> occurs) {
    	JFTSHierarchyNode node = null;
    	
        if (cls.canAs( OntClass.class )  &&  !occurs.contains( cls )) {
        	
        	Iterator<OntClass> i = cls.listSubClasses(true);
        	if (i.hasNext()){
        		
        		List<JFTSHierarchyNode> aux = new ArrayList<JFTSHierarchyNode>();
        		JFTSHierarchyNode nodeAux = null;        		
        		
        		while(i.hasNext()){
        			OntClass sub = i.next();
        			
                    // we push this expression on the occurs list before we recurse
                    occurs.add(cls);
                    nodeAux = _getSubClasses(sub, occurs);
                    occurs.remove(cls);
                    
                    if (nodeAux != null){
                    	aux.add(nodeAux);
                    }
        		}
        		node = new JFTSHierarchyNode(cls.getLocalName(), cls.listInstances(true).toList().size(), (aux.size() > 0) ? aux : null);        		
        	}
        	else{
        		node = new JFTSHierarchyNode(cls.getLocalName(), cls.listInstances(true).toList().size(), null);        		
        	}
        }
        return node;
    }	
	

	private Sort _processResultOrder(ResultOrder orders) throws JFTSSearcherException {
		Sort result = null;
		if (orders == null){
			result = new Sort();
		}
		else{
			String type = null;
			if (orders.getOrders().size() == 1){
				Map<String, String> args = orders.getOrders().get(0);
				type = args.get("type");
				if (type == null || type.isEmpty()){
					throw new JFTSSearcherException("Error, invalid search type order");
				}
				else{
					OrderTypes o = OrderTypes.valueOf(type.toUpperCase().trim());
					
					if (o == OrderTypes.INDEX){
						if (_validateOrder(args)){
							result = Sort.INDEXORDER;
						}
						else{
							throw new JFTSSearcherException("Error, the order isn't valid");							
						}
					}
					else{
						if (o == OrderTypes.RELEVANCE){
							if (_validateOrder(args)){
								result = Sort.RELEVANCE;
							}
							else{
								throw new JFTSSearcherException("Error, the order isn't valid");								
							}
						}
						else{
							String field = args.get("name");
							String dir = args.get("dir");
							if ((field == null || field.isEmpty()) || ( dir == null || dir.isEmpty())){
								throw new JFTSSearcherException("Error, invalid search order, field isn't valid");
							}
							else{
								if (_validateOrder(args)){								
									if (dir.trim().equalsIgnoreCase("desc")){
										result = new Sort(field, true);
									}
									else{
										result =new Sort(field);
									}
								}
								else{
									throw new JFTSSearcherException("Error, the order isn't valid");									
								}
							}
						}
					}
				}
			}
			else{
				List<SortField> fields = new ArrayList<SortField>();
				List<Map<String, String>> aux = orders.getOrders();
				
				for(Map<String, String> item : aux){
					type = item.get("type");
					if (type == null || type.isEmpty()){
						throw new JFTSSearcherException("Error, invalid search order");
					}
					else{
						OrderTypes o = OrderTypes.valueOf(type.toUpperCase().trim());
						if (o != OrderTypes.FIELD){
							throw new JFTSSearcherException("Error, invalid search field order");
						}
						else{
							String field = item.get("name");
							String dir = item.get("dir");
							if ((field == null || field.isEmpty()) || ( dir == null || dir.isEmpty())){
								throw new JFTSSearcherException("Error, invalid search order, field isn't valid");
							}
							else{
								if (dir.trim().equalsIgnoreCase("desc")){
									result = new Sort(field, true);
									fields.add(new SortField(field, true));
								}
								else{
									fields.add(new SortField(field));
								}
							}
						}
					}
				}
				SortField[] sf = (SortField[]) fields.toArray(new SortField[0]);				
				result = new Sort(sf);
			}
		}
		return result;
	}
	
	private boolean _validateOrder(Map<String, String> args) throws JFTSSearcherException {
		boolean valid = false;
		try {
			boolean validateOrders = config.getSearcherConfig().getUseValidOrders();
			if (validateOrders){
				List<OrderConfig> orders = config.getSearcherConfig().getOrders().getOrders();
				String type = null;
				for(OrderConfig o : orders){
					type = args.get("type");
					if (type.equalsIgnoreCase(o.getArgs().get("type"))){
						if (type.equalsIgnoreCase(OrderTypes.INDEX.toString())){
							valid = true;
						}
						else{
							if (type.equalsIgnoreCase(OrderTypes.RELEVANCE.toString())){
								valid = true;
							}
							else{
								if (type.equalsIgnoreCase(OrderTypes.FIELD.toString())){
									if (args.get("name").trim().equalsIgnoreCase(o.getArgs().get("name"))){
										if (args.get("dir").trim().equalsIgnoreCase(o.getArgs().get("dir"))){
											valid = true;
										}
									}
								}
							}
						}
					}
				}
				return valid;				
			}
			else{
				return true;
			}
		}
		catch (JFTSConfigException e) {
			throw new JFTSSearcherException(e.getMessage());			
		}
	}
	
	private JFTSResult _executeQuery(Query q, ResultOrder order, int page, int resultsPerPage) throws JFTSSearcherException {
		JFTSResult result = null;
		try{
			Sort o = _processResultOrder(order);						
			TopFieldDocCollector collector = new TopFieldDocCollector(ir, o,  page * resultsPerPage);
			is.search(q, collector);
			ScoreDoc[] docs = collector.topDocs().scoreDocs;
			
			int startResult = (page - 1) * resultsPerPage;
			
			if (startResult < 0 || startResult > docs.length){
				result = new JFTSResult(0, null);
			}
			else{
				int end = Math.min(docs.length, maxDocumentReturn);
				Document doc = null;
				IJFTSResultItem item = null;
				List<IJFTSResultItem> aux = new ArrayList<IJFTSResultItem>();
				
				for (int i = startResult; i < end; i++) {
					doc = is.doc(docs[i].doc);
					item = _getJFTSResultItem(doc);
					aux.add(item);
				}
				result = new JFTSResult(collector.getTotalHits(), aux);
			}
		}
		catch (IOException e) {
			e.printStackTrace();
			throw new JFTSSearcherException(e.getMessage());
		}
		catch(Throwable e){
			e.printStackTrace();
		}
		
		return result;		
	}
	
	private IJFTSResultItem _getJFTSResultItem(Document doc) throws JFTSSearcherException {
		IJFTSResultItem item = null;
		String className = null;
		
		try {
			className = config.getSearcherConfig().getJftsResultItemClass();
			Class<? extends IJFTSResultItem> infClass;
			infClass = Class.forName(className).asSubclass(IJFTSResultItem.class);
			item = infClass.newInstance();
			item.processDocument(doc);
		}
		catch (JFTSConfigException e) {
			throw new JFTSSearcherException(e.getMessage());
		}
		catch (ClassNotFoundException e) {
			throw new JFTSSearcherException("Error, cant't find class: " + className);
		}
		catch (InstantiationException e) {
			throw new JFTSSearcherException("Error to load class: " + className);
		}
		catch (IllegalAccessException e) {
			throw new JFTSSearcherException("Error to load class: " + className);
		}
		
		return item;
	}
	
	private JFTSAnalyzer _getAnalyzer(String language) throws JFTSSearcherException {
		JFTSAnalyzer analyzer = null;
		try {
			String unsupportedLanguage = config.getLanguageDetectorConfig().getUnknownLanguage();
			AnalyzerManager dinamicAnalyzer = new AnalyzerManager();

			if (language.trim().equalsIgnoreCase(unsupportedLanguage.trim())){
				if (config.getSearcherConfig().getUseDefaultAnalyzer()){
					language = dinamicAnalyzer.getDefaultLanguage();
				}
			}
			analyzer = dinamicAnalyzer.getAnalyzer(language);
		}
		catch (JFTSConfigException e) {
			throw new JFTSSearcherException(e.getMessage());			
		}
		catch (AnalyzerManagerException e) {
			throw new JFTSSearcherException(e.getMessage());			
		}
		return analyzer;
	}


	@Override
	public String getOntClassPath(String className) throws JFTSSearcherException, JFTSFrameworkException {
		String result = null;
		try {
			result = sm.getOntClassPath(className, sm.getBaseModel());
		}
		catch (JFTSSemanticException e) {
			throw new JFTSSearcherException(e.getMessage());			
		}
		return result;
	}	
}